using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.Diagnostics;


namespace Dragoon
{
    public interface IViewDrawable
    {
        void Draw(Matrix viewMatrix);
    }

    public interface ITerrain
    {
        int Width { get; }
        int Length { get; }
        float HeightAt(int x, int y);
        VertexMultiTextured[] Vertices { get; }
    }


    public class TerrainLevel
    {
        public readonly string ResourceName;
        public readonly float HeightCenter;
        public readonly float HeightBand;
        public TerrainLevel(string name, float center, float band)
        {
            ResourceName = name;
            HeightCenter = center;
            HeightBand = band;
        }
    }

    public class Terrain : EffectComponent, IViewDrawable, ITerrain
    {
        readonly IHeightMap _heightMap;
        readonly ICamera _camera;

        VertexBuffer _vertexBuffer;
        IndexBuffer _indexBuffer;
        VertexMultiTextured[] _vertices;
        VertexDeclaration _vertexDeclaration;
        Texture2D _levelTwoTex;
        Texture2D _levelOneTex;
        Texture2D _slopeTex;
        Texture2D _levelThreeTex;
        readonly TerrainLevel _levelOne, _levelTwo, _levelThree;
        readonly string _slopeName;


        public Terrain(Game game,
            IHeightMap heightMap,
            ICamera camera,
            TerrainLevel levelOne,
            TerrainLevel levelTwo,
            TerrainLevel levelThree,
            string slopeName)
            : base(game)
        {
            _heightMap = heightMap;
            _camera = camera;
            _levelOne = levelOne;
            _levelTwo = levelTwo;
            _levelThree = levelThree;
            _slopeName = slopeName;
        }

        public VertexMultiTextured[] Vertices
        {
            get { return _vertices; }
        }

        public int Width
        {
            get { return _heightMap.Width; }
        }

        public int Length
        {
            get { return _heightMap.Length; }
        }

        public float HeightAt(int x, int y)
        {
            return _heightMap[x, y];
        }

        private void LoadTextures()
        {
            _levelOneTex = Game.Content.Load<Texture2D>(_levelOne.ResourceName);
            _levelTwoTex = Game.Content.Load<Texture2D>(_levelTwo.ResourceName);
            _levelThreeTex = Game.Content.Load<Texture2D>(_levelThree.ResourceName);
            _slopeTex = Game.Content.Load<Texture2D>(_slopeName);

        }

        private void CalculateNormals(int[] indices)
        {
            for (int i = 0; i < _vertices.Length; i++)
                _vertices[i].Normal = new Vector3(0, 1, 2);
            for (int i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = _vertices[index2].Position - _vertices[index1].Position;
                Vector3 side2 = _vertices[index2].Position - _vertices[index3].Position;
                Vector3 normal = Vector3.Cross(side1, side2);

                _vertices[index1].Normal += normal;
                _vertices[index2].Normal += normal;
                _vertices[index3].Normal += normal;
            }

            for (int i = 0; i < _vertices.Length; i++)
                _vertices[i].Normal.Normalize();
        }



        protected override void LoadContent()
        {
            LoadVertices();
            LoadTextures();

        }


        private void CopyToTerrainBuffers(VertexMultiTextured[] vertices, int[] indices)
        {
            _vertexBuffer = new VertexBuffer(GraphicsDevice, vertices.Length * VertexMultiTextured.SizeInBytes, BufferUsage.WriteOnly);
            _vertexBuffer.SetData(vertices);

            _indexBuffer = new IndexBuffer(GraphicsDevice, typeof(int), indices.Length, BufferUsage.WriteOnly);
            _indexBuffer.SetData(indices);
        }



        private void LoadVertices()
        {
            _heightMap.Initialise();
            SetUpTerrainVertices();
            int[] terrainIndices = SetUpTerrainIndices();
            CalculateNormals(terrainIndices);
            SetUpTerrainTexWeights();
            CopyToTerrainBuffers(_vertices, terrainIndices);
            _vertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexMultiTextured.VertexElements);
        }

        enum DiagonalType { AC, BD }

        private DiagonalType ConvexDiagonal(Vector3 a, Vector3 b, Vector3 c, Vector3 d)
        {
          if (a.Y + c.Y > b.Y + d.Y)
            return DiagonalType.AC;
          else
            return DiagonalType.BD;
        }

        /// <summary>
        /// Returns 6 vertex points that covers the quad - 3 for each triangle
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b">clockwise from a</param>
        /// <param name="c">clockwise from a and b</param>
        /// <param name="d">colckwise from a, b and c</param>
        /// <returns></returns>
        private Vector3[] ConvexTriangles(Vector3 a, Vector3 b, Vector3 c, Vector3 d)
        {
          Vector3[] result = new Vector3[6];
          int i = 0;
          if (ConvexDiagonal(a, b, c, d) == DiagonalType.AC)
          {
            result[i++] = c;
            result[i++] = a;
            result[i++] = d;
            result[i++] = b;
            result[i++] = a;
            result[i++] = c;
          }
          else
          {
            result[i++] = d;
            result[i++] = b;
            result[i++] = a;
            result[i++] = c;
            result[i++] = b;
            result[i++] = d;
          }
          return result;
        }


        private Vector3 CalcPositionAt(int x, int y)
        {
          Vector3 result = Vector3.Zero;
          CalcPositionAt(x, y, ref result);
          return result;
        }

        public Vector3 NormalAt(int x, int y)
        {
          return _vertices[x + y * Width].Normal;
        }

        public Vector3 PositionAt(int x, int y)
        {
          return _vertices[x + y * Width].Position;
        }

        private int IndexOf(Vector3 v)
        {
          var r = (int)(v.X - v.Z * _heightMap.Width);
          Debug.Assert(r > -1 && r < Width * Length);
          return r;
        }


        private int[] SetUpTerrainIndices()
        {
          int[] indices = new int[(_heightMap.Width - 1) * (Length - 1) * 6];
          int counter = 0;
          for (int y = 0; y < _heightMap.Length - 1; y++)
          {
            for (int x = 0; x < _heightMap.Width - 1; x++)
            {
              var a = CalcPositionAt(x, y);
              var b = CalcPositionAt(x + 1, y);
              var c = CalcPositionAt(x + 1, y + 1);
              var d = CalcPositionAt(x, y + 1);
              var tri = ConvexTriangles(a, b, c, d);

              int i = 0;
              while (i < tri.Length)
                indices[counter++] = IndexOf(tri[i++]);
            }
          }

          return indices;
        }

        private float GetTexWeight(int x, int y, float i, float j)
        {
            return MathHelper.Clamp(1.0f - Math.Abs(_heightMap[x, y] - i) / j, 0.0f, 1.0f);
        }

        private void CalcPositionAt(int x, int y, ref Vector3 result)
        {
            result.X = (float)x;          // x grows towards the right
            result.Y = _heightMap[x, y];  // y grows up
            result.Z = (float)-y;         // z grows forward
        }


        private void SetUpTerrainVertices()
        {
            _vertices = new VertexMultiTextured[_heightMap.Width * _heightMap.Length];
            Vector3 pos = Vector3.Zero;
            for (int x = 0; x < _heightMap.Width; x++)
            {
                for (int y = 0; y < _heightMap.Length; y++)
                {
                    CalcPositionAt(x, y, ref pos);
                    _vertices[x + y * _heightMap.Width].Position = pos;
                    _vertices[x + y * _heightMap.Width].TextureCoordinate.X = (float)x / 30.0f;
                    _vertices[x + y * _heightMap.Width].TextureCoordinate.Y = (float)y / 30.0f;

                }
            }
        }

        private void SetUpTerrainTexWeights()
        {
            for (int x = 0; x < _heightMap.Width; x++)
            {
                for (int y = 0; y < _heightMap.Length; y++)
                {

                    if (_vertices[x + y * _heightMap.Width].Normal.Y > 0.6f)
                    {
                        _vertices[x + y * _heightMap.Width].TexWeights.X = GetTexWeight(x, y, 0f, 8f);
                        _vertices[x + y * _heightMap.Width].TexWeights.Y = GetTexWeight(x, y, 12f, 6f);
                        _vertices[x + y * _heightMap.Width].TexWeights.Z = 1.0f - _vertices[x + y * _heightMap.Width].Normal.Y;
                        _vertices[x + y * _heightMap.Width].TexWeights.W = GetTexWeight(x, y, 30f, 6f);
                    }
                    else
                    {
                        _vertices[x + y * _heightMap.Width].TexWeights.Z = 1.0f;
                        _vertices[x + y * _heightMap.Width].TexWeights.W = GetTexWeight(x, y, 30f, 6f);
                    }
                    // normalise fuzzy weights -- to sum up to one
                    float total = _vertices[x + y * _heightMap.Width].TexWeights.X
                        + _vertices[x + y * _heightMap.Width].TexWeights.Y
                        + _vertices[x + y * _heightMap.Width].TexWeights.Z
                        + _vertices[x + y * _heightMap.Width].TexWeights.W;

                    _vertices[x + y * _heightMap.Width].TexWeights.X /= total;
                    _vertices[x + y * _heightMap.Width].TexWeights.Y /= total;
                    _vertices[x + y * _heightMap.Width].TexWeights.Z /= total;
                    _vertices[x + y * _heightMap.Width].TexWeights.W /= total;

                }
            }
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        public void Draw(Matrix viewMatrix)
        {

            _effect.CurrentTechnique = _effect.Techniques["MultiTextured"];
            _effect.Parameters["xTexture0"].SetValue(_levelOneTex);
            _effect.Parameters["xTexture1"].SetValue(_levelTwoTex);
            _effect.Parameters["xTexture2"].SetValue(_slopeTex);
            _effect.Parameters["xTexture3"].SetValue(_levelThreeTex);
            _effect.Parameters["xView"].SetValue(viewMatrix);

            _effect.Begin();
            foreach (EffectPass pass in _effect.CurrentTechnique.Passes)
            {
                pass.Begin();

                GraphicsDevice.VertexDeclaration = _vertexDeclaration;
                GraphicsDevice.Indices = _indexBuffer;
                GraphicsDevice.Vertices[0].SetSource(_vertexBuffer, 0, VertexMultiTextured.SizeInBytes);
                int noVertices = _vertexBuffer.SizeInBytes / VertexMultiTextured.SizeInBytes;
                int noTriangles = _indexBuffer.SizeInBytes / sizeof(int) / 3;
                GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, noVertices, 0, noTriangles);
                pass.End();
            }
            _effect.End();
        }

        public override void Draw(GameTime gameTime)
        {
            Draw(_camera.ViewMatrix);
        }
    }
}
